package com.common.utils.classload;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * @author xin.ding
 * @date 2021/5/21 9:59
 */
@Slf4j
public class ClassLoaderUtils {
    /** 路径加载是否是否包含子目录中jar */
    private static final boolean INCLUDE_SUB_PATH = true;

    /** Class loader 重置 */
    private static ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();

    /**
     * Property name of the entry in JAR manifest file that describes the Flink specific entry point.
     */
    public static final String MANIFEST_ATTRIBUTE_ASSEMBLER_CLASS = "program-class";

    /**
     * Property name of the entry in JAR manifest file that describes the class with the main method.
     */
    public static final String MANIFEST_ATTRIBUTE_MAIN_CLASS = "Main-Class";

    /**
     * 获取父加载器优先的类加载器
     *
     * @param paths 加载的路径
     * @return
     */
    public static URLClassLoader parentFirst(String... paths) {
        Assert.isTrue(ArrayUtil.isNotEmpty(paths), "类加载的路径不能为空.");
        final URL[] urLs = getURLs(paths);

        final ParentFirstClassLoader parentFirstClassLoader = new ParentFirstClassLoader(urLs, getSystemClassLoader());
        Console.log("ClassLoader:{} URLS:{}", parentFirstClassLoader, parentFirstClassLoader.getURLs());
        return parentFirstClassLoader;
    }

    /**
     * 获取子加载器优先的类加载器
     *
     * @param paths 加载的路径
     * @return
     */
    public static URLClassLoader childFirst(String... paths) {
        Assert.isTrue(ArrayUtil.isNotEmpty(paths), "类加载的路径不能为空.");
        final URL[] urLs = getURLs(paths);

        final ChildFirstClassLoader childFirstClassLoader = new ChildFirstClassLoader(urLs, getSystemClassLoader(), null);
        Console.log("ClassLoader:{} URLS:{}", childFirstClassLoader, childFirstClassLoader.getURLs());
        return childFirstClassLoader;
    }

    /**
     * 获取只使用子加载器进行加载的类加载器
     *
     * @param paths 加载的路径
     * @return
     */
    public static URLClassLoader childOnly(String... paths) {
        Assert.isTrue(ArrayUtil.isNotEmpty(paths), "类加载的路径不能为空.");
        final URL[] urLs = getURLs(paths);

        final ChildFirstClassLoader childFirstClassLoader = new ChildFirstClassLoader(urLs, getExtClassLoader(), null);
        Console.log("ClassLoader:{} URLS:{}", childFirstClassLoader, childFirstClassLoader.getURLs());
        return childFirstClassLoader;

    }

    /**
     * 获取系统(应用)类加载器
     *
     * @return
     */
    public static ClassLoader getSystemClassLoader() {
        return ClassLoader.getSystemClassLoader();
    }

    /**
     * 获取Ext加载器
     *
     * @return
     */
    public static ClassLoader getExtClassLoader() {
        return getSystemClassLoader().getParent();
    }

    /**
     * 获取文件及路及的URL
     *
     * @param paths
     * @return
     */
    public static URL[] getURLs(String[] paths) {
        Assert.isTrue(null != paths && 0 != paths.length, "路径不能为空.");

        List<URL> urls = new ArrayList<>();
        List<String> dirs = new ArrayList<>(16);
        for (String path : paths) {
            if (path == null) {
                continue;
            }
            // 如果是文件则直接加载到URL列表中
            if (FileUtil.isFile(path)) {
                urls.add(URLUtil.getURL(FileUtil.file(path)));
                continue;
            }

            dirs.add(path);
            if (INCLUDE_SUB_PATH) {
                collectDirs(path, dirs);
            }
        }

        for (String path : dirs) {
            urls.addAll(doGetURLs(path));
        }

        return urls.toArray(new URL[0]);
    }

    /***
     * 递归寻找目录下的所有文件夹
     * @param path
     * @param collector
     */
    private static void collectDirs(String path, List<String> collector) {
        if (StrUtil.isEmpty(path)) {
            return;
        }

        File current = new File(path);
        if (!current.exists() || !current.isDirectory()) {
            return;
        }

        for (File child : current.listFiles()) {
            if (!child.isDirectory()) {
                continue;
            }

            collector.add(child.getAbsolutePath());
            collectDirs(child.getAbsolutePath(), collector);
        }
    }

    /**
     * 解析目录下所有Jar
     *
     * @param path
     * @return
     */
    private static List<URL> doGetURLs(final String path) {
        Assert.isTrue(!StrUtil.isBlank(path), "jar包路径不能为空.");

        File jarPath = FileUtil.file(path);

        Assert.isTrue(jarPath.exists() && jarPath.isDirectory(),
                "jar包路径必须存在且为目录.");

        /* set filter */
        FileFilter jarFilter = pathname -> pathname.getName().endsWith(".jar");

        /* iterate all jar */
        File[] allJars = jarPath.listFiles(jarFilter);
        List<URL> urls = new ArrayList<>(allJars.length);

        for (File allJar : allJars) {
            try {
                urls.add(allJar.toURI().toURL());
            } catch (Exception e) {
                throw new ClassLoadException("系统加载jar包出错", e);
            }
        }

        return urls;
    }

    /**
     * 使用ClassLoader运行Jar文件中main方法,不会新开启进程
     *
     * @param jarPath Jar文件路径
     * @param args    运行参数
     */
    public static void runJar(String jarPath, String libs, ClassLoaderUtils.ResolveType resolveType, String... args) {

        try {
            final File file = FileUtil.file(jarPath);
            URL url = file.toURI().toURL();

            final String entryPointClassName = getEntryPointClassNameFromJar(url);
            URLClassLoader jarLoader;
            switch (resolveType) {
                case CHILD_ONLY:
                    jarLoader = ClassLoaderUtils.childOnly(jarPath, libs);
                    break;
                case CHILD_FIRST:
                    jarLoader = ClassLoaderUtils.childFirst(jarPath, libs);
                    break;
                case PARENT_FIRST:
                    jarLoader = ClassLoaderUtils.parentFirst(jarPath, libs);
                    break;
                default:
                    throw new ClassLoadException("获取类加载失败");
            }

            Console.log(jarLoader.getURLs());

            final Class<?> mainClass = loadClass(entryPointClassName, jarLoader);
            callMainMethod(mainClass, args);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Jar 文件检查
     *
     * @param jar
     * @throws IOException
     */
    public static void checkJarFile(URL jar) throws IOException {
        File jarFile;
        try {
            jarFile = new File(jar.toURI());
        } catch (URISyntaxException e) {
            throw new IOException("JAR file path is invalid '" + jar + '\'');
        }
        if (!jarFile.exists()) {
            throw new IOException("JAR file does not exist '" + jarFile.getAbsolutePath() + '\'');
        }
        if (!jarFile.canRead()) {
            throw new IOException("JAR file can't be read '" + jarFile.getAbsolutePath() + '\'');
        }

        try (JarFile ignored = new JarFile(jarFile)) {
            // verify that we can open the Jar file
            log.debug("jar is ok");
        } catch (IOException e) {
            throw new IOException("Error while opening jar file '" + jarFile.getAbsolutePath() + '\'', e);
        }
    }


    /**
     * 判断class是否包含main方法
     *
     * @param entryClass
     * @return
     */
    private static boolean hasMainMethod(Class<?> entryClass) {
        Method mainMethod;
        try {
            mainMethod = entryClass.getMethod("main", String[].class);
        } catch (NoSuchMethodException e) {
            return false;
        } catch (Throwable t) {
            throw new RuntimeException("Could not look up the main(String[]) method from the class "
                    + entryClass.getName() + ": " + t.getMessage(), t);
        }

        return Modifier.isStatic(mainMethod.getModifiers()) && Modifier.isPublic(mainMethod.getModifiers());
    }

    /**
     * 调用Main函数
     *
     * @param entryClass main方法所在class
     * @param args       运行参数
     */
    private static void callMainMethod(Class<?> entryClass, String[] args) {
        Method mainMethod;
        if (!Modifier.isPublic(entryClass.getModifiers())) {
            throw new ClassLoadException("The class " + entryClass.getName() + " must be public.");
        }

        try {
            mainMethod = entryClass.getMethod("main", String[].class);
        } catch (NoSuchMethodException e) {
            throw new ClassLoadException("The class " + entryClass.getName() + " has no main(String[]) method.");
        } catch (Exception t) {
            throw new ClassLoadException("Could not look up the main(String[]) method from the class "
                    + entryClass.getName() + ": " + t.getMessage(), t);
        }

        if (!Modifier.isStatic(mainMethod.getModifiers())) {
            throw new ClassLoadException("The class " + entryClass.getName() + " declares a non-static main method.");
        }
        if (!Modifier.isPublic(mainMethod.getModifiers())) {
            throw new ClassLoadException("The class " + entryClass.getName() + " declares a non-public main method.");
        }

        try {
            mainMethod.invoke(null, (Object) args);
        } catch (IllegalArgumentException e) {
            throw new ClassLoadException("Could not invoke the main method, arguments are not matching.", e);
        } catch (IllegalAccessException e) {
            throw new ClassLoadException("Access to the main method was denied: " + e.getMessage(), e);
        } catch (InvocationTargetException e) {
            Throwable exceptionInMethod = e.getTargetException();
            if (exceptionInMethod instanceof Error) {
                throw (Error) exceptionInMethod;
            } else if (exceptionInMethod instanceof ClassLoadException) {
                throw (ClassLoadException) exceptionInMethod;
            } else {
                throw new ClassLoadException("The main method caused an error: " + exceptionInMethod.getMessage(), exceptionInMethod);
            }
        } catch (Throwable t) {
            throw new ClassLoadException("An error occurred while invoking the program's main method: " + t.getMessage(), t);
        }
    }

    /**
     * 获取Jar包中的main class
     *
     * @param jarFile
     * @return
     * @throws ClassLoadException
     */
    private static String getEntryPointClassNameFromJar(URL jarFile) throws ClassLoadException {
        JarFile jar;
        Manifest manifest;
        String className;

        // Open jar file
        try {
            jar = new JarFile(new File(jarFile.toURI()));
        } catch (URISyntaxException use) {
            throw new ClassLoadException("Invalid file path '" + jarFile.getPath() + "'", use);
        } catch (IOException ioex) {
            throw new ClassLoadException("Error while opening jar file '" + jarFile.getPath() + "'. "
                    + ioex.getMessage(), ioex);
        }

        // jar file must be closed at the end
        try {
            // Read from jar manifest
            try {
                manifest = jar.getManifest();
            } catch (IOException ioex) {
                throw new ClassLoadException("The Manifest in the jar file could not be accessed '"
                        + jarFile.getPath() + "'. " + ioex.getMessage(), ioex);
            }

            if (manifest == null) {
                throw new ClassLoadException("No manifest found in jar file '" + jarFile.getPath() + "'. The manifest is need to point to the program's main class.");
            }

            Attributes attributes = manifest.getMainAttributes();

            // check for a "program-class" entry first
            className = attributes.getValue(MANIFEST_ATTRIBUTE_ASSEMBLER_CLASS);
            if (className != null) {
                return className;
            }

            // check for a main class
            className = attributes.getValue(MANIFEST_ATTRIBUTE_MAIN_CLASS);
            if (className != null) {
                return className;
            } else {
                throw new ClassLoadException("Neither a '" + MANIFEST_ATTRIBUTE_MAIN_CLASS + "', nor a '"
                        + MANIFEST_ATTRIBUTE_ASSEMBLER_CLASS + "' entry was found in the jar file.");
            }
        } finally {
            try {
                jar.close();
            } catch (Exception t) {
                throw new ClassLoadException("Could not close the JAR file: " + t.getMessage(), t);
            }
        }
    }

    /***
     * 加载class
     * @param className
     * @param cl
     * @return
     * @throws ClassLoadException
     */
    private static Class<?> loadClass(String className, ClassLoader cl) throws ClassLoadException {
        ClassLoader contextCl = null;
        try {
            contextCl = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(cl);
            return Class.forName(className, false, cl);
        } catch (ClassNotFoundException e) {
            throw new ClassLoadException("The program's entry point class '" + className
                    + "' was not found in the jar file.", e);
        } catch (ExceptionInInitializerError e) {
            throw new ClassLoadException("The program's entry point class '" + className
                    + "' threw an error during initialization.", e);
        } catch (LinkageError e) {
            throw new ClassLoadException("The program's entry point class '" + className
                    + "' could not be loaded due to a linkage failure.", e);
        } catch (Exception t) {
            throw new ClassLoadException("The program's entry point class '" + className
                    + "' caused an exception during initialization: " + t.getMessage(), t);
        } finally {
            if (contextCl != null) {
                Thread.currentThread().setContextClassLoader(contextCl);
            }
        }
    }


    /**
     * 类加载顺序
     */
    public enum ResolveType {

        /** 优先使用子类加载器 */
        CHILD_FIRST,
        /** 优先使用父加载器,默认双亲委派原则 */
        PARENT_FIRST,
        /** 只使用子类加载器去加载 */
        CHILD_ONLY;

        /**
         * 获取类加载顺序
         *
         * @param resolveOrder
         * @return
         */
        public static ResolveType fromString(String resolveOrder) {
            if (resolveOrder.equalsIgnoreCase("parent-first")) {
                return PARENT_FIRST;
            } else if (resolveOrder.equalsIgnoreCase("child-first")) {
                return CHILD_FIRST;
            } else {
                throw new IllegalArgumentException("Unknown resolve order: " + resolveOrder);
            }
        }
    }

}
